package server;

import game.Card;
import game.Player;
import game.server.Game;
import game.server.ServerTable;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.Semaphore;


import server.utils.CommandType;
import server.utils.PlayerWatcher;

public class ClientSession extends Thread {
	private Socket socket;
	private ObjectOutputStream netOut;
	private ObjectInputStream netIn;
	private ActiveSessions activeSessions;
	private OutboundMessages outMessages;
	private OutboundMessages inMessages;
	private PlayerWatcher playerWatcher;
	private Semaphore maxPlayers;
	private Game game;
	/**
	 * True - client waits for its turn
	 * False - its client's turn
	 */
	private boolean waitTurn;
	private Object turnSync;
	private Player player;
	private ServerTable table;
	/**
	 * 
	 * When client connected it has to wait until game is started.
	 * @param socket
	 * @param activeSessions
	 * @param outMessages
	 * @param game
	 * @throws IOException
	 */
	public ClientSession(Socket socket, ActiveSessions activeSessions, 
			OutboundMessages outMessages, OutboundMessages inMessages,
			Game game) throws IOException {
		this.socket = socket;
		this.activeSessions = activeSessions;
		this.outMessages = outMessages;
		this.inMessages = inMessages;
		this.game = game;
		this.playerWatcher = game.getPlayerWatcher();
		this.table = game.getTable();
		netOut = new ObjectOutputStream(socket.getOutputStream());
		netIn = new ObjectInputStream(socket.getInputStream());
		Server.log.println("Client connected...", Server.conf.isConsole());
		waitTurn = true;
		turnSync = new Object();
		this.maxPlayers = game.getMaxPlayers();
		start();
	}
	
	
	@Override
	public void run() {
		try {
			maxPlayers.acquire(); //wait
			authorizeClient(); //until name is unique
			game.getStartLatch().countDown(); //thread is ready
			playerWatcher.notifyGame(); //notify game if its in 
										//pause to get more players
			
			clientLoop: while (socket.isConnected()) {
				Object fromClient = netIn.readObject();
				if (fromClient != null) {
					Message msg = (Message) fromClient;
					if (!this.isWaitTurn()) {
						switch(msg.getType()) {
						case CHAT:
							outMessages.addMessage(msg);
							break;
						case OK:
							inMessages.addMessage(msg);
							break;
						case BET:
							inMessages.addMessage(msg);
							outMessages.addMessage(new Message(
									msg.getFrom(), CommandType.BET,
									msg.getCommandValue()));
							break;
						case ERROR:
							inMessages.addMessage(msg);
							break;
						case QUIT:
							break clientLoop;
						default:
							outMessages.addMessage(
									new Message(Server.conf.getServerName(),
											msg.getFrom(), CommandType.ERROR,
											"No such command available!"));
						}
					} else {
						switch(msg.getType()) {
						case CHAT: //if client is waiting for turn send chat messages to all
							outMessages.addMessage(msg);
							break;
						case OK:
							inMessages.addMessage(msg);
							break;
						case ERROR:
							inMessages.addMessage(msg);
							break;
						case QUIT:
							break clientLoop;
						default:
							outMessages.addMessage(
									new Message(Server.conf.getServerName(),
											msg.getFrom(), CommandType.ERROR,
											"No such command available!"));
						}
					}
				}
			}
			
		} catch (ClassNotFoundException e) {
			Server.log.println("Class not found exception @ thread - " + 
					getName(), Server.conf.isConsole());
		} catch (IOException e) {
			outMessages.addMessage(new Message(getName(), 
					CommandType.ERROR, getName() + " - crashed..."));
		} catch (InterruptedException e) {
			Server.log.println("Interrupted exception @ thread - " + 
					getName(), Server.conf.isConsole());
		} finally {
			try {
				outMessages.addMessage(new Message(getName(), 
						CommandType.EDITPLAYER, getName() + " - left."));
				game.getStartLatch().leave();
				maxPlayers.release();
				try {
					table.removePlayer(player);
				} catch (NullPointerException e) {
					Server.log.println("Couldn't remove player from table" +
							"@ Clientsession.run() --> ended",
							Server.conf.isConsole());
				}
				removeClient(); //from activesessions
				playerWatcher.checkPlayerCount();
				inMessages.addMessage(new Message(this.getName(),
						Server.conf.getServerName(),
						CommandType.QUIT, "Disconnecting!")); //game.notify
				socket.close();
			} catch (IOException e) {}
		}
	}
	/**
	 * Removes client from activeSessions list.
	 */
	public void removeClient() {
		synchronized (activeSessions) {
			Iterator<ClientSession> active = activeSessions.iterator();
			while(active.hasNext()) {
				ClientSession cli = active.next();
				if (cli.getName().equalsIgnoreCase(this.getName())) {
					active.remove();
					Server.log.println(cli + " removed...",
							Server.conf.isConsole());
				}
			}
		}
	}
	/**
	 * Method for authorizing client.
	 * Waits until client sends unique name to server.
	 * Sends ERROR message to client if duplicate name exists in server's 
	 * client list. Sends OK message to client if no duplicates exist.
	 * Sends INFO message to all notifying that client was authorixed.
	 * Adds client to active sessions list and sets thread's name.
	 * Creates player object.
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private void authorizeClient() throws IOException, ClassNotFoundException {
		while(true) {
			Object fromClient = netIn.readObject(); //wait
			if (fromClient != null) {
				Message msg = (Message) fromClient;
				String name = msg.getFrom();
				boolean duplicate = false;
				synchronized (activeSessions) {
					Iterator<ClientSession> active = 
						activeSessions.iterator();
					while(active.hasNext()) {
						if (active.next().getName().equalsIgnoreCase(name)) {
							duplicate = true;
						}
					}
				}
				if (!duplicate) {
					super.setName(name);
					this.sendMessage(new Message(Server.conf.getServerName(), 
							CommandType.OK, "Client authorized"));
					this.sendMessage(new Message(Server.conf.getServerName(),
							this.getName(), CommandType.PLAYERS, table.getPlayers()));
					activeSessions.addSession(this);
					outMessages.addMessage(new Message(name, 
							CommandType.EDITPLAYER, "Client \"" + name + "\" authorized..."));
					player = new Player(name, Server.conf.getInitialMoney());
					table.addPlayerToWaitlist(player);
					break;
				} else {
					this.sendMessage(new Message(Server.conf.getServerName(), 
							CommandType.ERROR, "This name already exists!"));
				}
				
			}
		}
	}
	/**
	 * Method for sending messages to current client.
	 * Message is an object.
	 * @param msg Message to send
	 */
	public void sendMessage(Message msg) {
		try {
			if (!socket.isClosed()) {
				netOut.writeObject(msg);
			} else {
				throw new IOException(); 			// tegelikult: CALL catch()
			}
		} catch (IOException eee) {
			outMessages.addMessage(new Message(getName(), 
					CommandType.ERROR, getName() + " - crashed..."));
			try {
				socket.close();
			} catch (IOException ee) {}
		}
	}


	/**
	 * @return True if client is in wait for its turn. False if not.
	 */
	public boolean isWaitTurn() {
		synchronized (turnSync) {
			return waitTurn;
		}
	}


	/**
	 * @param waitTurn set this client to wait for its turn
	 */
	public void setWaitTurn(boolean waitTurn) {
		synchronized (turnSync) {
			Message msg = new Message(Server.conf.getServerName(),
					this.getName(), CommandType.TURNCHANGE, 
					Boolean.toString(waitTurn));
			sendMessage(msg);
			Server.log.println(msg.toString(), Server.conf.isConsole());
			this.waitTurn = waitTurn;
		}
	}
	/**
	 * @return the player
	 */
	public synchronized Player getPlayer() {
		return player;
	}

}
